home *** CD-ROM | disk | FTP | other *** search
/ Celestin Apprentice 5 / Apprentice-Release5.iso / Source Code / C / Applications / Python 1.3.3 / Python 133 SRC / Lib / mimify.py < prev    next >
Text File  |  1996-02-14  |  11KB  |  416 lines

  1. #!/usr/local/bin/python
  2.  
  3. '''Mimification and unmimification of mail messages.
  4.  
  5. decode quoted-printable parts of a mail message or encode using
  6. quoted-printable.
  7.  
  8. Usage:
  9.     mimify(input, output)
  10.     unmimify(input, output)
  11. to encode and decode respectively.  Input and output may be the name
  12. of a file or an open file object.  Only a readline() method is used
  13. on the input file, only a write() method is used on the output file.
  14. When using file names, the input and output file names may be the
  15. same.
  16.  
  17. Interactive usage:
  18.     mimify.py -e [infile [outfile]]
  19.     mimify.py -d [infile [outfile]]
  20. to encode and decode respectively.  Infile defaults to standard
  21. input and outfile to standard output.
  22. '''
  23.  
  24. # Configure
  25. MAXLEN = 200    # if lines longer than this, encode as quoted-printable
  26. CHARSET = 'ISO-8859-1'    # default charset for non-US-ASCII mail
  27. QUOTE = '> '        # string replies are quoted with
  28. # End configure
  29.  
  30. import regex, regsub, string
  31.  
  32. qp = regex.compile('^content-transfer-encoding:[\000-\377]*quoted-printable',
  33.            regex.casefold)
  34. mp = regex.compile('^content-type:[\000-\377]*multipart/[\000-\377]*boundary="?\\([^;"\n]*\\)',
  35.            regex.casefold)
  36. chrset = regex.compile('^\\(content-type:.*charset="\\)\\(us-ascii\\|iso-8859-[0-9]+\\)\\("[\000-\377]*\\)',
  37.                regex.casefold)
  38. he = regex.compile('^-*$')
  39. mime_code = regex.compile('=\\([0-9a-f][0-9a-f]\\)', regex.casefold)
  40. mime_head = regex.compile('=\\?iso-8859-1\\?q\\?\\([^?]+\\)\\?=',
  41.               regex.casefold)
  42. repl = regex.compile('^subject:[ \t]+re: ', regex.casefold)
  43.  
  44. class File:
  45.     '''A simple fake file object that knows about limited
  46.        read-ahead and boundaries.
  47.        The only supported method is readline().'''
  48.  
  49.     def __init__(self, file, boundary):
  50.         self.file = file
  51.         self.boundary = boundary
  52.         self.peek = None
  53.  
  54.     def readline(self):
  55.         if self.peek is not None:
  56.             return ''
  57.         line = self.file.readline()
  58.         if not line:
  59.             return line
  60.         if self.boundary:
  61.             if line == self.boundary + '\n':
  62.                 self.peek = line
  63.                 return ''
  64.             if line == self.boundary + '--\n':
  65.                 self.peek = line
  66.                 return ''
  67.         return line
  68.  
  69. class HeaderFile:
  70.     def __init__(self, file):
  71.         self.file = file
  72.         self.peek = None
  73.  
  74.     def readline(self):
  75.         if self.peek is not None:
  76.             line = self.peek
  77.             self.peek = None
  78.         else:
  79.             line = self.file.readline()
  80.         if not line:
  81.             return line
  82.         if he.match(line) >= 0:
  83.             return line
  84.         while 1:
  85.             self.peek = self.file.readline()
  86.             if len(self.peek) == 0 or \
  87.                (self.peek[0] != ' ' and self.peek[0] != '\t'):
  88.                 return line
  89.             line = line + self.peek
  90.             self.peek = None
  91.  
  92. def mime_decode(line):
  93.     '''Decode a single line of quoted-printable text to 8bit.'''
  94.     newline = ''
  95.     while 1:
  96.         i = mime_code.search(line)
  97.         if i < 0:
  98.             break
  99.         newline = newline + line[:i] + \
  100.               chr(string.atoi(mime_code.group(1), 16))
  101.         line = line[i+3:]
  102.     return newline + line
  103.  
  104. def mime_decode_header(line):
  105.     '''Decode a header line to 8bit.'''
  106.     newline = ''
  107.     while 1:
  108.         i = mime_head.search(line)
  109.         if i < 0:
  110.             break
  111.         match = mime_head.group(0, 1)
  112.         newline = newline + line[:i] + mime_decode(match[1])
  113.         line = line[i + len(match[0]):]
  114.     return newline + line
  115.  
  116. def unmimify_part(ifile, ofile):
  117.     '''Convert a quoted-printable part of a MIME mail message to 8bit.'''
  118.     multipart = None
  119.     quoted_printable = 0
  120.     is_repl = 0
  121.     if ifile.boundary and ifile.boundary[:2] == QUOTE:
  122.         prefix = QUOTE
  123.     else:
  124.         prefix = ''
  125.  
  126.     # read header
  127.     hfile = HeaderFile(ifile)
  128.     while 1:
  129.         line = hfile.readline()
  130.         if not line:
  131.             return
  132.         if prefix and line[:len(prefix)] == prefix:
  133.             line = line[len(prefix):]
  134.             pref = prefix
  135.         else:
  136.             pref = ''
  137.         line = mime_decode_header(line)
  138.         if qp.match(line) >= 0:
  139.             quoted_printable = 1
  140.             continue    # skip this header
  141.         ofile.write(pref + line)
  142.         if not prefix and repl.match(line) >= 0:
  143.             # we're dealing with a reply message
  144.             is_repl = 1
  145.         if mp.match(line) >= 0:
  146.             multipart = '--' + mp.group(1)
  147.         if he.match(line) >= 0:
  148.             break
  149.     if is_repl and (quoted_printable or multipart):
  150.         is_repl = 0
  151.  
  152.     # read body
  153.     while 1:
  154.         line = ifile.readline()
  155.         if not line:
  156.             return
  157.         line = regsub.gsub(mime_head, '\\1', line)
  158.         if prefix and line[:len(prefix)] == prefix:
  159.             line = line[len(prefix):]
  160.             pref = prefix
  161.         else:
  162.             pref = ''
  163. ##        if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
  164. ##            multipart = line[:-1]
  165.         while multipart:
  166.             if line == multipart + '--\n':
  167.                 ofile.write(pref + line)
  168.                 multipart = None
  169.                 line = None
  170.                 break
  171.             if line == multipart + '\n':
  172.                 ofile.write(pref + line)
  173.                 nifile = File(ifile, multipart)
  174.                 unmimify_part(nifile, ofile)
  175.                 line = nifile.peek
  176.                 continue
  177.             # not a boundary between parts
  178.             break
  179.         if line and quoted_printable:
  180.             while line[-2:] == '=\n':
  181.                 line = line[:-2]
  182.                 newline = ifile.readline()
  183.                 if newline[:len(QUOTE)] == QUOTE:
  184.                     newline = newline[len(QUOTE):]
  185.                 line = line + newline
  186.             line = mime_decode(line)
  187.         if line:
  188.             ofile.write(pref + line)
  189.  
  190. def unmimify(infile, outfile):
  191.     '''Convert quoted-printable parts of a MIME mail message to 8bit.'''
  192.     if type(infile) == type(''):
  193.         ifile = open(infile)
  194.         if type(outfile) == type('') and infile == outfile:
  195.             import os
  196.             d, f = os.path.split(infile)
  197.             os.rename(infile, os.path.join(d, ',' + f))
  198.     else:
  199.         ifile = infile
  200.     if type(outfile) == type(''):
  201.         ofile = open(outfile, 'w')
  202.     else:
  203.         ofile = outfile
  204.     nifile = File(ifile, None)
  205.     unmimify_part(nifile, ofile)
  206.     ofile.flush()
  207.  
  208. mime_char = regex.compile('[=\240-\377]') # quote these chars in body
  209. mime_header_char = regex.compile('[=?\240-\377]') # quote these in header
  210.  
  211. def mime_encode(line, header):
  212.     '''Code a single line as quoted-printable.
  213.        If header is set, quote some extra characters.'''
  214.     if header:
  215.         reg = mime_header_char
  216.     else:
  217.         reg = mime_char
  218.     newline = ''
  219.     if len(line) >= 5 and line[:5] == 'From ':
  220.         # quote 'From ' at the start of a line for stupid mailers
  221.         newline = string.upper('=%02x' % ord('F'))
  222.         line = line[1:]
  223.     while 1:
  224.         i = reg.search(line)
  225.         if i < 0:
  226.             break
  227.         newline = newline + line[:i] + \
  228.               string.upper('=%02x' % ord(line[i]))
  229.         line = line[i+1:]
  230.     line = newline + line
  231.  
  232.     newline = ''
  233.     while len(line) >= 75:
  234.         i = 73
  235.         while line[i] == '=' or line[i-1] == '=':
  236.             i = i - 1
  237.         i = i + 1
  238.         newline = newline + line[:i] + '=\n'
  239.         line = line[i:]
  240.     return newline + line
  241.  
  242. mime_header = regex.compile('\\([ \t(]\\)\\([-a-zA-Z0-9_+]*[\240-\377][-a-zA-Z0-9_+\240-\377]*\\)\\([ \t)]\\|$\\)')
  243.  
  244. def mime_encode_header(line):
  245.     '''Code a single header line as quoted-printable.'''
  246.     newline = ''
  247.     while 1:
  248.         i = mime_header.search(line)
  249.         if i < 0:
  250.             break
  251.         newline = newline + line[:i] + mime_header.group(1) + \
  252.               '=?' + CHARSET + '?Q?' + \
  253.               mime_encode(mime_header.group(2), 1) + \
  254.               '?=' + mime_header.group(3)
  255.         line = line[i+len(mime_header.group(0)):]
  256.     return newline + line
  257.  
  258. mv = regex.compile('^mime-version:', regex.casefold)
  259. cte = regex.compile('^content-transfer-encoding:', regex.casefold)
  260. iso_char = regex.compile('[\240-\377]')
  261.  
  262. def mimify_part(ifile, ofile, is_mime):
  263.     '''Convert an 8bit part of a MIME mail message to quoted-printable.'''
  264.     has_cte = is_qp = 0
  265.     multipart = None
  266.     must_quote_body = must_quote_header = has_iso_chars = 0
  267.  
  268.     header = []
  269.     header_end = ''
  270.     message = []
  271.     message_end = ''
  272.     # read header
  273.     hfile = HeaderFile(ifile)
  274.     while 1:
  275.         line = hfile.readline()
  276.         if not line:
  277.             break
  278.         if not must_quote_header and iso_char.search(line) >= 0:
  279.             must_quote_header = 1
  280.         if mv.match(line) >= 0:
  281.             is_mime = 1
  282.         if cte.match(line) >= 0:
  283.             has_cte = 1
  284.             if qp.match(line) >= 0:
  285.                 is_qp = 1
  286.         if mp.match(line) >= 0:
  287.             multipart = '--' + mp.group(1)
  288.         if he.match(line) >= 0:
  289.             header_end = line
  290.             break
  291.         header.append(line)
  292.  
  293.     # read body
  294.     while 1:
  295.         line = ifile.readline()
  296.         if not line:
  297.             break
  298.         if multipart:
  299.             if line == multipart + '--\n':
  300.                 message_end = line
  301.                 break
  302.             if line == multipart + '\n':
  303.                 message_end = line
  304.                 break
  305.         if is_qp:
  306.             while line[-2:] == '=\n':
  307.                 line = line[:-2]
  308.                 newline = ifile.readline()
  309.                 if newline[:len(QUOTE)] == QUOTE:
  310.                     newline = newline[len(QUOTE):]
  311.                 line = line + newline
  312.             line = mime_decode(line)
  313.         message.append(line)
  314.         if not has_iso_chars:
  315.             if iso_char.search(line) >= 0:
  316.                 has_iso_chars = must_quote_body = 1
  317.         if not must_quote_body:
  318.             if len(line) > MAXLEN:
  319.                 must_quote_body = 1
  320.  
  321.     # convert and output header and body
  322.     for line in header:
  323.         if must_quote_header:
  324.             line = mime_encode_header(line)
  325.         if chrset.match(line) >= 0:
  326.             if has_iso_chars:
  327.                 # change us-ascii into iso-8859-1
  328.                 if string.lower(chrset.group(2)) == 'us-ascii':
  329.                     line = chrset.group(1) + \
  330.                            CHARSET + chrset.group(3)
  331.             else:
  332.                 # change iso-8859-* into us-ascii
  333.                 line = chrset.group(1) + 'us-ascii' + chrset.group(3)
  334.         if has_cte and cte.match(line) >= 0:
  335.             line = 'Content-Transfer-Encoding: '
  336.             if must_quote_body:
  337.                 line = line + 'quoted-printable\n'
  338.             else:
  339.                 line = line + '7bit\n'
  340.         ofile.write(line)
  341.     if (must_quote_header or must_quote_body) and not is_mime:
  342.         ofile.write('Mime-Version: 1.0\n')
  343.         ofile.write('Content-Type: text/plain; ')
  344.         if has_iso_chars:
  345.             ofile.write('charset="%s"\n' % CHARSET)
  346.         else:
  347.             ofile.write('charset="us-ascii"\n')
  348.     if must_quote_body and not has_cte:
  349.         ofile.write('Content-Transfer-Encoding: quoted-printable\n')
  350.     ofile.write(header_end)
  351.  
  352.     for line in message:
  353.         if must_quote_body:
  354.             line = mime_encode(line, 0)
  355.         ofile.write(line)
  356.     ofile.write(message_end)
  357.  
  358.     line = message_end
  359.     while multipart:
  360.         if line == multipart + '--\n':
  361.             return
  362.         if line == multipart + '\n':
  363.             nifile = File(ifile, multipart)
  364.             mimify_part(nifile, ofile, 1)
  365.             line = nifile.peek
  366.             ofile.write(line)
  367.             continue
  368.  
  369. def mimify(infile, outfile):
  370.     '''Convert 8bit parts of a MIME mail message to quoted-printable.'''
  371.     if type(infile) == type(''):
  372.         ifile = open(infile)
  373.         if type(outfile) == type('') and infile == outfile:
  374.             import os
  375.             d, f = os.path.split(infile)
  376.             os.rename(infile, os.path.join(d, ',' + f))
  377.     else:
  378.         ifile = infile
  379.     if type(outfile) == type(''):
  380.         ofile = open(outfile, 'w')
  381.     else:
  382.         ofile = outfile
  383.     nifile = File(ifile, None)
  384.     mimify_part(nifile, ofile, 0)
  385.     ofile.flush()
  386.  
  387. import sys
  388. if __name__ == '__main__' or (len(sys.argv) > 0 and sys.argv[0] == 'mimify'):
  389.     import getopt
  390.     usage = 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
  391.  
  392.     opts, args = getopt.getopt(sys.argv[1:], 'l:ed')
  393.     if len(args) not in (0, 1, 2):
  394.         print usage
  395.         sys.exit(1)
  396.     if (('-e', '') in opts) == (('-d', '') in opts):
  397.         print usage
  398.         sys.exit(1)
  399.     for o, a in opts:
  400.         if o == '-e':
  401.             encode = mimify
  402.         elif o == '-d':
  403.             encode = unmimify
  404.         elif o == '-l':
  405.             try:
  406.                 MAXLEN = string.atoi(a)
  407.             except:
  408.                 print usage
  409.                 sys.exit(1)
  410.     if len(args) == 0:
  411.         encode(sys.stdin, sys.stdout)
  412.     elif len(args) == 1:
  413.         encode(args[0], sys.stdout)
  414.     else:
  415.         encode(args[0], args[1])
  416.